M2.875 · Deep Learning · PEC1
2024-2 · Máster universitario en Ciencia de datos (Data science)
Estudios de Informática, Multimedia y Telecomunicación
A lo largo de esta práctica implementaremos varios modelos de redes neuronales, empleando Keras sobre la base de datos Imagenette (versión 320px). En concreto, abordaremos las siguientes tareas:
Consideraciones generales:
Formato de la entrega:
A lo largo de esta práctica vamos a implementar varios modelos de redes neuronales para clasificar las imágenes de la base de datos Imagenette.
La base de datos Imagenette es un subconjunto de 10 clases fácilmente clasificables de Imagenet, un proyecto que ha sido fundamental para avanzar en la investigación sobre visión artificial y aprendizaje profundo. Imagenette contiene alrededor de 13.000 imágenes de diferentes tamaños pertenecientes a 10 categorías (tench, English springer, cassette player, chain saw, church, French horn, garbage truck, gas pump, golf ball, parachute), cada una en una carpeta distinta.
Concretamente en esta PEC utilizaremos una versión (Imagenette2-320) que ha sido reescalada pero manteniendo el aspect ratio de cada imagen (se han ajustado de forma que la dimensión menor de cada imagen es 320 píxeles). Esto atenuará la carga computacional de los algoritmos al usar bases de datos de imágenes pero manteniendo la suficiente calidad necesaria para nuestros experimentos. Los datos vienen separados en 2 conjuntos, entrenamiento y validación.
Nota: Debido al uso de imágenes como datos en esta práctica, los entrenamientos de cada ejercicio se pueden demorar desde unos minutos a más de media hora utilizando GPU (los tiempos con CPU son sensiblemente más largos). Se recomienda realizar la práctica en el entorno que ofrece la plataforma Kaggle, ya que ofrece un entorno gratuito con 30 horas semanales de uso de GPU.
A lo largo de toda la práctica, para la creación de las distintas redes, iremos alternando el uso del modelo Sequential y el modelo Functional de Keras a través de las clases Sequential y Model respectivamente.
Se aconseja la lectura detallada de la documentación de ambos modelos para llevar a cabo la realización de la práctica.
Empezamos instalando y cargando las librerías más relevantes:
# Instalamos la última versión de Tensorflow (con CUDA)
%pip install tensorflow[and-cuda]
# Importamos Tensorflow
import tensorflow as tf
print("TensorFlow version : ", tf.__version__)
# Necesitaremos GPU
print("GPU is", "available" if tf.config.list_physical_devices('GPU') else "NOT AVAILABLE")
# Keras version is 3.5.0
from tensorflow import keras
print("Keras version : ", keras.__version__)
# Importamos los elementos de Keras que utilizaremos con mayor frecuencia
from keras.utils import image_dataset_from_directory
from keras.layers import (Input, Dense, Dropout, Flatten, Conv2D, Conv2DTranspose,
MaxPooling2D, UpSampling2D, Rescaling, Resizing,
RandomFlip, RandomRotation)
from keras.callbacks import EarlyStopping, ReduceLROnPlateau
from keras.optimizers import Adam
# Importamos algunas librerías que necesitaremos para la PEC
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
En este apartado exploraremos la base de datos y prepararemos la carga de las imágenes para los modelos de los siguientes apartados.
Para crear nuestra base de datos tenemos que descargar el archivo de imágenes desde el siguiente enlace (es un archivo .zip que ocupa aproximadamente 340 MB).
NOTA: para poder descargar el archivo de imágenes tenéis que iniciar sesión con el usuario y contraseña de la UOC.
A partir de aquí:
Si trabajamos en local simplemente tenemos que descomprimir el archivo descargado.
Si trabajamos desde Kaggle debemos subir el notebook del enunciado a la plataforma (para ello podéis seguir los 6 primeros pasos del siguiente artículo) y después, una vez subido el notebook, expandir la barra lateral desplegable de la derecha y en el menú 'Input' clickar el botón 'Upload' y subir el archivo descargado previamente. Después hay que darle un nombre a la base de datos y cuando se cargue el fichero ya tendréis accesible la base de datos en la ruta ../input/.
Una vez tenemos la base de datos accesible vamos a inspeccionarla.
En la carpeta /images (si trabajamos en local) o /kaggle/input/nombre-base-de-datos/images (si trabajamos desde Kaggle) encontramos 2 carpetas:
/train/ se encuentra el total de las imágenes de entrenamiento separadas por clases (cada clase en una carpeta distinta)./val/ se encuentra el total de las imágenes de validación separadas por clases (cada clase en una carpeta distinta).Como podemos observar, tenemos imágenes para realizar el entrenamiento y la validación de los modelos pero no tenemos un conjunto de prueba, lo crearemos durante en este primer apartado.
Comencemos obteniendo los datos y analizando su estructura y características.
En primer lugar, inspeccionaremos la organización de los datos.
train y val):
import os
from pathlib import Path
# Directorios (ajustar la ruta según ubicación de los datos)
train_path = '/kaggle/input/dataset-pec1/images/train'
val_path = '/kaggle/input/dataset-pec1/images/val'
# Listar clases en cada conjunto
clases_train = os.listdir(train_path)
clases_val = os.listdir(val_path)
print(f"Clases train: {clases_train}")
print(f"Clases val: {clases_val}")
# Contar total de imágenes por conjunto y por clase
# Imprimir número de imágenes por clase en train
lista_train = [(clase,len(os.listdir(Path(train_path)/clase))) for clase in clases_train]
total_train = sum([elem[1] for elem in lista_train])
print(f"Total de imágenes train: {total_train}")
print(f'Número de imágenes por clase en train:\n{lista_train}')
# Imprimir número de imágenes por clase en val
lista_val = [(clase,len(os.listdir(Path(val_path)/clase))) for clase in clases_val]
total_val = sum([elem[1] for elem in lista_val])
print(f"\nTotal de imágenes val: {total_val}")
print(f'Número de imágenes por clase en val:\n{lista_val}')
# FUENTES:
# https://stackoverflow.com/questions/48190959/how-do-i-append-a-string-to-a-path
# https://www.geeksforgeeks.org/python-os-listdir-method/
# Verificar que ambas listas de clases son iguales
clases_train.sort()
clases_val.sort()
if clases_train == clases_val:
print("Las listas de clases son iguales.")
else:
print("Las listas de clases son diferentes.")
import pandas as pd
# Representación gráfica de la distribución de clases
df = pd.DataFrame(lista_train, columns=['Clase','Frecuencia'])
df.plot(kind='bar', x='Clase', title='Train')
df = pd.DataFrame(lista_val, columns=['Clase','Frecuencia'])
df.plot(kind='bar', x='Clase', title='Val')
# FUENTES:
# https://stackoverflow.com/questions/45080698/make-frequency-histogram-from-list-with-tuple-elements
Ahora examinaremos el formato de las imágenes para entender su tamaño y rango de valores. Visualizaremos algunas imágenes de ejemplo de cada clase.
import random
from PIL import Image
random.seed(42)
def get_img_path(folder_path):
images = list(Path(folder_path).iterdir())
random_image = random.choice(images)
return random_image
dyn_range = []
for clase in clases_train:
img_train_path = get_img_path(Path(train_path)/clase)
#print(img_train_path)
image = Image.open(img_train_path)
#print(np.max(image),np.min(image))
dyn_range.append([np.max(image),np.min(image)])
plt.imshow(image)
plt.axis('off')
plt.show()
print(dyn_range)
# FUENTES:
# https://www.geeksforgeeks.org/show-random-picture-from-a-folder-in-python/
# https://medium.com/@ingaleashay/loading-and-displaying-images-in-google-colab-a-guide-with-opencv-pil-and-matplotlib-d13bf5b8fe6b
A continuación, prepararemos los datos para el entrenamiento con Keras. Usaremos la función **tf.keras.utils.image_dataset_from_directory()** de TensorFlow/Keras, que permite crear lotes de datos etiquetados a partir de directorios de imágenes organizados por clase.
La documentación de esta función se encuentra tanto en la web de Keras como en la de Tensorflow.
Esta función nos facilitará generar conjuntos de entrenamiento, validación y prueba a partir de las carpetas analizadas. Las imágenes serán redimensionadas a un tamaño fijo y organizadas en lotes (batch).
Especificaciones: Vamos a convertir las imágenes a tamaño 160×160 píxeles y agruparlas en lotes de 64. Mantendremos el rango dinámico original en esta etapa, ya que más adelante aplicaremos una capa de reescalado para normalizarlas (al rango 0-1). También separaremos parte de los datos de test a partir de los de validación.
tf.keras.utils.image_dataset_from_directory() para generar 3 conjuntos de datos a partir de las carpetas /train y /val:
train/. Redimensiona las imágenes a 160×160, con batch_size=64 i label_mode="categorical" (10 categorías).val/, con un validation_split de 0.5 usando subset="validation" en la función y fija un seed para reproducibilidad. De nuevo redimensiona las imágenes a 160×160, con batch_size=64 y label_mode="categorical".val/ usando subset="training" en la función (con el mismo seed para obtener la partición complementaria). Redimensiona también a 160×160 con batch_size=64 y usa label_mode="categorical".subset="validation" y subset="training" es completamente arbitraria y podría haberse realizado al revés. Lo importante es dividir los datos que se encuentran en la carpeta /val al 50% entre validación y test.
img_height, img_width = 160, 160
batch_size = 64
# Conjunto de entrenamiento
train_ds = tf.keras.utils.image_dataset_from_directory(
train_path,
label_mode="categorical",
image_size = (img_height, img_width),
batch_size = batch_size,
shuffle = True,
seed = 42)
# FUENTES:
# https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/image_dataset_from_directory
# Conjunto de validación y test (usamos el directorio de validación de Imagenette como test)
val_ds = tf.keras.utils.image_dataset_from_directory(
val_path,
label_mode="categorical",
image_size = (img_height, img_width),
batch_size = batch_size,
validation_split = 0.5,
subset = "validation",
seed = 42
)
test_ds = tf.keras.utils.image_dataset_from_directory(
val_path,
label_mode="categorical",
image_size = (img_height, img_width),
batch_size = batch_size,
validation_split = 0.5,
subset = "training",
seed = 42
)
# Comprobación de los resultados
print('Comprobación train_ds:')
print(train_ds.class_names)
for image_batch, labels_batch in train_ds:
print(image_batch.shape)
print(labels_batch.shape)
break
print('\nComprobación val_ds:')
print(val_ds.class_names)
for image_batch, labels_batch in val_ds:
print(image_batch.shape)
print(labels_batch.shape)
break
print('\nComprobación test_ds:')
print(test_ds.class_names)
for image_batch, labels_batch in test_ds:
print(image_batch.shape)
print(labels_batch.shape)
break
# FUENTES:
# https://www.tensorflow.org/tutorials/load_data/images?hl=es-419
Como primer modelo entrenaremos una red neuronal completamente conectada (red densa o
Arquitectura propuesta: Utilizaremos el API funcional de Keras (clase Model) para construir la red. Emplearemos las capas Resizing y Rescaling de Keras para preprocesar las imágenes, seguidas de Flatten para aplanar, y varias capas Dense intercaladas con Dropout para el clasificador.
En este apartado utilizaremos las capas Resizing, Rescaling, Flatten, Dense y Dropout de Keras.
summary()
# Construcción del modelo ANN completamente conectado (modelo funcional)
from keras import layers,Model,callbacks,optimizers
img_inputs = Input(shape=(160, 160, 3))
# Una capa que reduzca las dimensiones de entrada de (160,160) a (32,32).
resize = layers.Resizing(32,32,crop_to_aspect_ratio=True)
x = resize(img_inputs)
# Una capa de reescalado para que los valores de píxel queden entre 0 y 1.
x = layers.Rescaling(scale=1./255)(x)
# Una capa Flatten para convertir la imagen reducida en un vector 1D.
x = layers.Flatten()(x)
# Una capa densa completamente conectada que tenga un número de neuronas equivalente a dos tercios del tamaño de la capa anterior, activación ReLU.
x = layers.Dense(2048, activation="relu")(x)
# Una capa Dropout con probabilidad 0.5.
x = layers.Dropout(0.5)(x)
# Otra capa densa de la mitad del tamaño que la capa densa inmediatamente anterior, activación ReLU.
x = layers.Dense(1024, activation="relu")(x)
# Otra capa Dropout con probabilidad 0.5.
x = layers.Dropout(0.5)(x)
# Una capa de salida densa con el tamaño y función de activación adecuados para el problema de clasificación que se plantea.
outputs = layers.Dense(10, activation="softmax")(x)
model = Model(inputs=img_inputs, outputs=outputs, name="ANN_model")
model.summary()
# FUENTES:
# https://keras.io/guides/functional_api/
# https://keras.io/api/layers/
Procedemos a compilar y entrenar el modelo:
Empezamos con lr=1e-3
# Compilación del modelo
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss="categorical_crossentropy",
metrics=['accuracy']
)
# FUENTES:
# # https://www.tensorflow.org/guide/keras/training_with_built_in_methods
# https://keras.io/api/models/model_training_apis/
model.save('ANN_lr_1e-3.keras')
# Definir callbacks: EarlyStopping
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
# ReduceLROnPlateau
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-6
)
# Entrenamiento del modelo
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=100,
callbacks=[early_stopping,reduce_lr]
)
# FUENTES:
# https://www.tensorflow.org/tutorials/load_data/images?hl=es-419
# https://machinelearningmastery.com/how-to-stop-training-deep-neural-networks-at-the-right-time-using-early-stopping/
# Función auxiliar para graficar accuracy y loss
plt.plot(history.history['accuracy'])
plt.title('model accuracy')
plt.show()
plt.plot(history.history['loss'])
plt.title('model loss')
plt.show()
# Validación en el conjunto de prueba
eval = model.evaluate(test_ds)
print(eval)
Repetimos para lr=1e-4
# Crear un nuevo modelo sin pesos previos
model_1e_4 = Model(inputs=img_inputs, outputs=outputs, name="ANN_model_1e_4")
# Compilación del modelo
model_1e_4.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-4),
loss="categorical_crossentropy",#sparse_categorical_crossentropy
metrics=['accuracy']
)
# Definir callbacks: EarlyStopping
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
# ReduceLROnPlateau
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-6
)
# Entrenamiento del modelo
history_1e_4 = model_1e_4.fit(
train_ds,
validation_data=val_ds,
epochs=100,
callbacks=[early_stopping,reduce_lr]
)
# Validación en el conjunto de prueba
eval_1e_4 = model_1e_4.evaluate(test_ds)
print(eval_1e_4)
model_1e_4.save('ANN_lr_1e-4.keras')
Finalizamos con lr=1e-5
# Crear un nuevo modelo sin pesos previos
model_1e_5 = Model(inputs=img_inputs, outputs=outputs, name="ANN_model_1e_5")
# Compilación del modelo
model_1e_5.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-5),
loss="categorical_crossentropy",
metrics=['accuracy']
)
# Definir callbacks: EarlyStopping
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
# ReduceLROnPlateau
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-6
)
# Entrenamiento del modelo
history_1e_5 = model_1e_5.fit(
train_ds,
validation_data=val_ds,
epochs=100,
callbacks=[early_stopping,reduce_lr]
)
# Validación en el conjunto de prueba
eval_1e_5 = model_1e_5.evaluate(test_ds)
print(eval_1e_5)
model_1e_5.save('ANN_lr_1e-5.keras')
Ahora implementaremos una red neuronal convolucional (CNN) básica, que suele ser mucho más efectiva para clasificación de imágenes. Las CNN aprovechan la estructura espacial de los datos mediante capas concretas que permiten extraer características locales antes de la clasificación.
Arquitectura propuesta: Usaremos un modelo Keras Sequential para esta CNN. Constará de un bloque extractor de características y luego un clasificador denso similar al anterior pero más pequeño.
En este apartado utilizaremos las capas Conv2D, MaxPooling2D, Dense y Dropout de Keras.
Se proporciona el código del clasificador ya implementado.
# Definición del modelo CNN
cnn_model = keras.Sequential([
keras.layers.InputLayer(shape=(img_height, img_width, 3)),
Rescaling(1./255),
Conv2D(16, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(32, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(64, 3, padding='same', activation='relu'),
MaxPooling2D(),
Dropout(0.2),
Dense(64, activation='relu'),
Dropout(0.5),
Dense(10, activation='softmax')
], name="CNN_model")
cnn_model.summary()
# Compilar modelo CNN
cnn_model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-4),
loss="sparse_categorical_crossentropy",
metrics=['accuracy']
)
# Definir callbacks EarlyStopping y ReduceLROnPlateau
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-6
)
# Entrenamiento del modelo CNN
#history_cnn = cnn_model.fit(
# train_ds,
# validation_data=val_ds,
# epochs=100,
# callbacks=[early_stopping,reduce_lr]
#)
# Escribe aquí el modelo corregido
cnn_model = keras.Sequential([
keras.layers.InputLayer(shape=(img_height, img_width, 3)),
Rescaling(1./255),
Conv2D(16, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(32, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(64, 3, padding='same', activation='relu'),
MaxPooling2D(),
Dropout(0.2),
Flatten(),
Dense(64, activation='relu'),
Dropout(0.5),
Dense(10, activation='softmax')
], name="CNN_model")
cnn_model.summary()
# Compilar modelo CNN
cnn_model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-4),
loss="categorical_crossentropy",
metrics=['accuracy']
)
# Definir callbacks EarlyStopping y ReduceLROnPlateau
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-6
)
# Entrenamiento del modelo CNN
history_cnn = cnn_model.fit(
train_ds,
validation_data=val_ds,
epochs=100,
callbacks=[early_stopping,reduce_lr]
)
cnn_model.save('cnn_model.keras')
# Resultados
plt.plot(history_cnn.history['accuracy'])
plt.title('model accuracy')
plt.show()
plt.plot(history_cnn.history['loss'])
plt.title('model loss')
plt.show()
eval_cnn = cnn_model.evaluate(test_ds)
print(eval_cnn)
Aunque la CNN entrenada es bastante efectiva, podemos intentar mejorar su generalización aumentando artificialmente el tamaño y la diversidad del conjunto de entrenamiento mediante técnicas de aumentación de datos. La aumentación consiste en aplicar transformaciones aleatorias a las imágenes (giros, rotaciones, zoom, etc.) de modo que el modelo reciba variantes de las imágenes originales en cada época, simulando tener más datos
Keras proporciona capas de preprocesamiento de imagen que realizan estas transformaciones de forma eficiente durante el entrenamiento, por ejemplo RandomFlip, RandomRotation, RandomZoom entre otras.
Aquí utilizaremos algunas de estas capas para implementar la aumentación. En particular, probaremos con voltear horizontalmente las imágenes y aplicar pequeñas rotaciones aleatorias.
RandomFlip que voltee aleatoriamente las imágenes horizontalmente.RandomRotation con factor de rotación de 0.1 (±10%).# Modelo de aumentación de datos
more_data_model = keras.Sequential([
keras.layers.InputLayer(shape=(img_height, img_width, 3)),
keras.layers.RandomFlip(mode="horizontal", seed=42),
keras.layers.RandomRotation(factor=0.1, seed=42)
], name="more_data_model")
more_data_model.summary()
# Tomar un batch de entrenamiento y obtener una imagen
batch = train_ds.take(1).get_single_element()
images, labels = batch
plt.imshow(images[0].numpy().astype("uint8"))
plt.axis('off')
plt.show()
# Aplicar aumentación varias veces y visualizar
image_v1 = more_data_model(images)
plt.imshow(image_v1[0].numpy().astype("uint8"))
plt.axis('off')
plt.show()
image_v2 = more_data_model(images)
plt.imshow(image_v2[0].numpy().astype("uint8"))
plt.axis('off')
plt.show()
image_v3 = more_data_model(images)
plt.imshow(image_v3[0].numpy().astype("uint8"))
plt.axis('off')
plt.show()
image_v4 = more_data_model(images)
plt.imshow(image_v4[0].numpy().astype("uint8"))
plt.axis('off')
plt.show()
# FUENTES:
# https://stackoverflow.com/questions/48126690/how-to-make-tf-data-dataset-return-all-of-the-elements-in-one-call
# https://www.tensorflow.org/tutorials/load_data/images?hl=es-419
Ahora incorporaremos la capa de aumentación al modelo CNN para entrenarlo con datos aumentados en cada época.
Rescaling y la primera Conv2D del modelo CNN anterior. Es decir, modifica la arquitectura para que las imágenes de entrada, tras ser reescaladas, pasen por las capas de flip y rotation aleatorias, y luego continúen por la CNN. A continuación, compila y entrena el nuevo modelo CNN aumentado siguiendo las mismas indicaciones que en el ejercicio anterior, excepto que esta vez usaremos un learning rate inicial mayor (1e-3). Mantén EarlyStopping, ReduceLROnPlateau, 100 épocas, etc. Luego evalúa el modelo final en el conjunto de test.
Comenta las diferencias con el modelo sin aumentación en términos de:
# Modelo CNN con aumentación de datos
cnn_more_data_model = keras.Sequential([
keras.layers.InputLayer(shape=(img_height, img_width, 3)),
Rescaling(1./255),
more_data_model,
Conv2D(16, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(32, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(64, 3, padding='same', activation='relu'),
MaxPooling2D(),
Dropout(0.2),
Flatten(),
Dense(64, activation='relu'),
Dropout(0.5),
Dense(10, activation='softmax')
], name="cnn_more_data_model")
cnn_more_data_model.summary()
# Compilación de la red
cnn_more_data_model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss="categorical_crossentropy",
metrics=['accuracy']
)
# Entrenamiento
# Definir callbacks EarlyStopping y ReduceLROnPlateau
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-3
)
# Entrenamiento del modelo CNN
history_cnn = cnn_more_data_model.fit(
train_ds,
validation_data=val_ds,
epochs=100,
callbacks=[early_stopping,reduce_lr]
)
cnn_more_data_model.save('cnn_more_data_model.keras')
# Resultados
plt.plot(history_cnn.history['accuracy'])
plt.title('model accuracy')
plt.show()
plt.plot(history_cnn.history['loss'])
plt.title('model loss')
plt.show()
eval_cnn = cnn_more_data_model.evaluate(test_ds)
print(eval_cnn)
# Definición de función de preprocesado de imágenes
def preprocess_image_for_sr(filepath):
# Leemos imagen
img = tf.io.read_file(filepath)
img = tf.image.decode_jpeg(img, channels=3)
# Conversión de las imágenes a float32 para normalizar entre 0 y 1
img = tf.image.convert_image_dtype(img, tf.float32)
# Definición de tamaño de imágenes hr(320x320) y lr(80x80)
hr = tf.image.resize(img, [320, 320], method=tf.image.ResizeMethod.BICUBIC)
lr = tf.image.resize(img, [80, 80], method=tf.image.ResizeMethod.BICUBIC)
# Normalización de valores del tensor entre 0 y 1
hr = tf.clip_by_value(hr, 0.0, 1.0)
lr = tf.clip_by_value(lr, 0.0, 1.0)
return lr, hr
# Creación de subconjuntos de entrenamiento y de validación
train_files = []
val_files = []
for cls in clases_train:
cls_files = sorted((Path(train_path)/cls).glob("*.*"))
# Para cada clase, separación del subconjunto de entrenamiento (80%) y validación (20%)
split_idx = int(len(cls_files) * 0.2)
val_files += [str(p) for p in cls_files[:split_idx]]
train_files += [str(p) for p in cls_files[split_idx:]]
random.shuffle(train_files)
random.shuffle(val_files)
# Preparación de subconjuntos de entrenamiento y test para optimizar procesado
train_sr_ds = tf.data.Dataset.from_tensor_slices(train_files).map(preprocess_image_for_sr, num_parallel_calls=tf.data.AUTOTUNE).batch(32).prefetch(tf.data.AUTOTUNE)
val_sr_ds = tf.data.Dataset.from_tensor_slices(val_files).map(preprocess_image_for_sr, num_parallel_calls=tf.data.AUTOTUNE).batch(32).prefetch(tf.data.AUTOTUNE)
Hasta ahora hemos trabajado en un problema de clasificación. En los apartados restantes abordaremos un problema distinto pero relacionado con la visión: la superresolución de imágenes. La superresolución consiste en generar una imagen de alta resolución (HR) a partir de una de baja resolución (LR), intentando recuperar o inferir los detalles perdidos al reducir la imagen. Es un problema de aprendizaje supervisado donde el modelo aprende una transformación imagen -> imagen.
Usaremos nuevamente la base de datos Imagenette para crear ejemplos de entrenamiento: a partir de cada imagen original (320px) generaremos una versión reducida (p. ej. 80px) que servirá como entrada, teniendo como salida esperada la imagen original. De este modo, el modelo aprenderá a mapear de baja a alta resolución. En lugar de una red convolucional para clasificación, necesitaremos una red capaz de procesar una imagen de entrada y producir una imagen de salida. Las capas transpuestas de convolución (Conv2DTranspose) o técnicas de upsampling son las piezas clave para estos modelos, ya que permiten aumentar las dimensiones espaciales de los datos.
A continuación, crearemos el conjunto de datos para superresolución y entrenaremos una CNN de superresolución simple.
Primero generaremos los pares de imágenes de entrenamiento y validación para superresolución. Partiremos de las imágenes originales de entrenamiento (y validación) a su resolución completa y, por simplicidad, las redimensionaremos a un tamaño fijo de 320×320 (ignorando la relación de aspecto original, similar a lo hecho en clasificación) para utilizarlas como imágenes de referencia de alta resolución (HR), y las reduciremos a 1/4 de su tamaño (aprox. 80×80) para usarlas como imágenes de entrada de baja resolución (LR). Para realizar ambas transformaciones (ajuste de las imágenes originales a 320x320 y su reducción a 80x80) utilizaremos métodos de interpolación bicúbica para simular imágenes degradas suavemente.
Procedemos a obtener las rutas de imágenes de entrenamiento y validación, luego creamos un Dataset aplicando la función de mapeo que realiza la lectura y transformación:
/train/ (para crear los subconjuntos de entrenamiento y validación) a su tamaño completo (320px) como HR, y genera imágenes LR reduciéndolas a 1/4 de su tamaño lineal (∼80×80). Se pide:
Procedemos a verificar la base de datos:
# Verificación de un par LR-HR
for lr_batch, hr_batch in train_sr_ds.take(1):
lr_img = lr_batch[0].numpy()
hr_img = hr_batch[0].numpy()
print("LR shape:", lr_img.shape, "HR shape:", hr_img.shape)
print("LR pixel range:", lr_img.min(), "-", lr_img.max())
print("HR pixel range:", hr_img.min(), "-", hr_img.max())
break
Ahora definimos un modelo CNN para superresolución. Optaremos por una arquitectura simple con UpSampling2D para escalar la imagen gradualmente. Implementamos el modelo:
UpSampling2D (factor=2) o Conv2DTranspose con stride=2 para doblar el ancho y alto (80->160).# Modelo de Super-Resolución (80px -> 320px)
super_resolution_model = keras.Sequential([
# Entrada
keras.layers.InputLayer(shape=(80, 80, 3)),
# Capas Conv2D para extraer características en la resolución baja
Conv2D(16, 3, padding='same', activation='relu'),
Conv2D(32, 3, padding='same', activation='relu'),
# Capa Conv2DTranspose con stride=2 (80->160)
UpSampling2D(size=(2, 2)),
Conv2D(64, 3, padding='same', activation='relu'),
# Capa Conv2D para refinar, seguida de otra capa de upsampling 2× (160->320)
UpSampling2D(size=(2, 2)),
# Capas Conv2D finales para reconstruir la imagen de salida de 320×320×3
Conv2D(32, 3, padding='same', activation='relu'),
Conv2D(3, 3, padding='same', activation='linear')
], name="super_resolution_model")
super_resolution_model.summary()
# Compilación de la red
super_resolution_model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss="mse"
)
# Entrenamiento de la red
# Definir callbacks EarlyStopping y ReduceLROnPlateau
early_stopping = callbacks.EarlyStopping(
monitor='val_loss',
patience=5,
restore_best_weights=True
)
reduce_lr = callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.2,
patience=5,
min_lr=1e-5
)
# Entrenamiento del modelo CNN
history_cnn = super_resolution_model.fit(
train_sr_ds,
validation_data=val_sr_ds,
epochs=50,
callbacks=[early_stopping,reduce_lr]
)
super_resolution_model.save('super_resolution_model.keras')
#super_resolution_model = keras.models.load_model('super_resolution_model.keras')
# Resultados
plt.plot(history_cnn.history['loss'])
plt.title('model loss')
plt.show()
Con el modelo de superresolución entrenado, evaluaremos su desempeño tanto cuantitativamente (con métricas) como cualitativamente (visualizando imágenes). Para ello realizaremos la inferencia: esto es, tomar imágenes LR de test, pasarlas por el modelo sr_model para obtener imágenes superresueltas, y compararlas con las HR originales. Calcularemos la métrica PSNR (Peak Signal-to-Noise Ratio) para cuantificar la calidad.
PSNR se mide en decibelios (dB) y valores más altos indican mayor similitud con la imagen original (por ejemplo, >30 dB suele indicar muy buena calidad de reconstrucción).
También visualizaremos algunos ejemplos, mostrando la imagen de baja resolución, la superresuelta por la CNN y la original de alta resolución, para inspeccionar los detalles a simple vista.
tf.image.psnr) y obtén su valor medio.tf.image.resize() junto con method=tf.image.ResizeMethod.NEAREST_NEIGHBOR), la imagen generada por el modelo (SR) y la imagen HR original, para poder comparar la calidad visualmente a parte del valor de la PSNR.# Preparar conjunto de test para SR (a partir de imagenette/val directory)
test_files = []
for cls in clases_val:
cls_files = sorted((Path(train_path)/cls).glob("*.*"))
test_files += [str(p) for p in cls_files]
random.shuffle(test_files)
# Preparación de conjunto de test para optimizar procesado (batch size = 1)
test_sr_ds = tf.data.Dataset.from_tensor_slices(test_files).map(preprocess_image_for_sr, num_parallel_calls=tf.data.AUTOTUNE).batch(1).prefetch(tf.data.AUTOTUNE)
# Verificación de un par LR-HR
for lr_batch, hr_batch in test_sr_ds.take(1):
lr_img = lr_batch[0].numpy()
hr_img = hr_batch[0].numpy()
print("LR shape:", lr_img.shape, "HR shape:", hr_img.shape)
print("LR pixel range:", lr_img.min(), "-", lr_img.max())
print("HR pixel range:", hr_img.min(), "-", hr_img.max())
break
# Lista para almacenar la PSNR de cada imagen
resultados_psnr = []
# Recorrer el dataset de test (batch size = 1)
for lr,hr in test_sr_ds:
# Generar la imagen de superresolución a partir de la imagen LR
sr_image = super_resolution_model(lr)
# Quitar la dimensión de batch y asegurar que los valores estén en [0,1]
sr_image = tf.squeeze(sr_image, axis=0)
sr_image = tf.clip_by_value(sr_image, 0.0, 1.0)
# Convertir la imagen HR a array de numpy (quitamos la dimensión de batch)
hr = np.array(hr)
# Calcular la PSNR entre la imagen SR generada y la imagen HR original
psnr = tf.image.psnr(hr, sr_image, max_val=1).numpy() #tensor->numpy
# Almacenar el valor de PSNR en la lista
resultados_psnr.append(psnr)
# Calcular la PSNR media del conjunto de test
avg_psnr = np.mean(resultados_psnr)
print("PSNR media en el conjunto de test: {:.2f} dB".format(avg_psnr))
import math
# Elegir algunas imágenes de test aleatorias para visualización
sample_clases = random.sample(clases_val,3)
sample_img = []
for cls in sample_clases:
cls_files = sorted((Path(val_path)/cls).glob("*.*"))
sample_img.append(random.choice(cls_files))
# Para cada imagen
psnr = []
for img in sample_img:
# Leer y procesar una imagen de test
lr, hr = preprocess_image_for_sr(str(img))
# Generar superresolución
sr_image = super_resolution_model(tf.expand_dims(lr, axis=0))
# Calcular PSNR
sr_image = tf.squeeze(sr_image, axis=0)
sr_image = tf.clip_by_value(sr_image, 0.0, 1.0)
hr = np.array(hr)
psnr = tf.image.psnr(hr, sr_image, max_val=1).numpy()
print(f"PSNR: {psnr}")
# Preparar imágenes para visualizar del mismo tamaño
lr_resized = tf.image.resize(lr, [320, 320], method='nearest')
sr_resized = tf.image.resize(sr_image, [320, 320], method='nearest')
# Las imágenes están en [0,1], escalar a [0,255] para mostrar correctamente
lr_scaled = (lr_resized * 255).numpy().astype("uint8")
sr_scaled = (sr_resized * 255).numpy().astype("uint8")
# Mostrar las imágenes
plt.imshow(lr_scaled)
plt.axis('off')
plt.show()
plt.imshow(sr_scaled)
plt.axis('off')
plt.show()
Los avances recientes en superresolución han producido arquitecturas más complejas (p. ej., basadas en Redes Generativas Adversarias) que logran resultados notablemente mejores, a costa de un entrenamiento costoso. Uno de estos modelos es ESRGAN (Enhanced Super-Resolution GAN por Xintao Wang et al., entrenado en grandes bases de datos de imágenes HD (como DIV2K) para lograr superresolución 4× con gran fidelidad. Afortunadamente, podemos aprovechar un modelo pre-entrenado en lugar de entrenar uno desde cero.
Vamos a emplear un modelo pre-entrenado de ESRGAN disponible vía TensorFlow Hub
Este modelo ha sido entrenado en el conjunto DIV2K (imágenes de alta calidad) con degradación bicúbica, por lo que está especializado en producir imágenes 4× más grandes con notable detalle.
Usaremos ESRGAN para aplicar superresolución a las mismas imágenes de test y compararemos los resultados con nuestro modelo implementado en el ejercicio 5.
"https://tfhub.dev/captain-pool/esrgan-tf2/1").import tensorflow_hub as hub
import kagglehub
# Download latest version
path = kagglehub.model_download("kaggle/esrgan-tf2/tensorFlow2/esrgan-tf2")
print("Path to model files:", path)
# Cargamos el modelo ESRGAN desde TF-Hub:
#esrgan = hub.load("https://tfhub.dev/captain-pool/esrgan-tf2/1")
#print("Modelo ESRGAN cargado.")
esrgan = hub.load('/kaggle/input/esrgan-tf2/tensorflow2/esrgan-tf2/1')
from tqdm import tqdm
# Lista para almacenar la PSNR de cada imagen usando ESRGAN
resultados_psnr_esrgan = []
# Recorrer el dataset de test (batch size = 1)
total_steps = sum(1 for _ in test_sr_ds) # Total de iteraciones conocidas
for lr, hr in tqdm(test_sr_ds, total=total_steps, desc="Procesando imágenes"):
# Preparar la imagen LR para ESRGAN:
# ESRGAN espera entrada en rango [0,255] como float32, y test_sr_ds tiene imágenes en [0,1]
lr_scaled = (lr * 255).numpy().astype("float32")
# Generar la imagen de superresolución con ESRGAN
sr_image = esrgan(lr_scaled)
# Quitar la dimensión de batch y normalizar la salida a [0,1]
sr_image = tf.squeeze(sr_image, axis=0)
sr_image = tf.clip_by_value(sr_image, 0.0, 1.0)
# Obtener la imagen HR correspondiente (quitamos la dimensión de batch)
hr = np.array(hr)
# Calcular la PSNR entre la imagen generada por ESRGAN y la imagen HR original
psnr = tf.image.psnr(hr, sr_image, max_val=1).numpy()
resultados_psnr_esrgan.append(psnr)
# Calcular la PSNR media en el conjunto de test con ESRGAN
avg_psnr_esrgan = np.mean(resultados_psnr_esrgan)
print("PSNR media en el conjunto de test con ESRGAN: {:.2f} dB".format(avg_psnr_esrgan))
# FUENTE:
# https://www.datacamp.com/tutorial/tqdm-python
# Aplicamos ESRGAN a las mismas imágenes de ejemplo que usamos con nuestro modelo
# Para cada imagen
# Preparar la imagen LR de test
# Convertir salida a [0,1] float
# Calcular PSNR comparado con HR
# Visualizar comparativa
# Para cada imagen
psnr = []
for img in sample_img:
# Leer y procesar una imagen de test
lr, hr = preprocess_image_for_sr(str(img))
# Generar superresolución
sr_image = esrgan(tf.expand_dims(lr, axis=0))
# Calcular PSNR
sr_image = tf.squeeze(sr_image, axis=0)
sr_image = tf.clip_by_value(sr_image, 0.0, 1.0)
hr = np.array(hr)
psnr = tf.image.psnr(hr, sr_image, max_val=1).numpy()
print(f"PSNR: {psnr}")
# Preparar imágenes para visualizar del mismo tamaño
lr_resized = tf.image.resize(lr, [320, 320], method='nearest')
sr_resized = tf.image.resize(sr_image, [320, 320], method='nearest')
# Las imágenes están en [0,1], escalar a [0,255] para mostrar correctamente
lr_scaled = (lr_resized * 255).numpy().astype("uint8")
sr_scaled = (sr_resized * 255).numpy().astype("uint8")
# Mostrar las imágenes
plt.imshow(lr_scaled)
plt.axis('off')
plt.show()
plt.imshow(sr_scaled)
plt.axis('off')
plt.show()
En esta práctica hemos explorado tanto la clasificación de imágenes con redes neuronales (densas vs convolucionales) como la superresolución con CNNs, aplicándolo todo sobre la base de datos Imagenette.